# -*- coding: utf-8 -*-

'''
/**
 * @addtogroup FileSysOpt
 * FileSysOpt
 * @{
 */
/**
 * @file        FileSysOpt.py
 * @brief       ZipOpt.py正式更名为 FileSysOpt.py
 * @note        XXXX
 * @author      靳普诏(puzhao.jin@hopechart.com)
 * @date        2023/08/31
 * @version     1.0

 * @par         修改日志
 * <table>
 * <tr><th>Date         <th>Version     <th>Author      <th> Description
 * <tr><td>2023/08/31   <td>1.0         <td>靳普诏       <td> 创建初始版本
 * <tr><td>2024/01/05   <td>2.0         <td>靳普诏       <td> 文件更名为 FileSysOpt.py
 * @copyright   xxxx
 */
'''

import sys
import os
import zipfile
import shutil
import requests  
from requests.exceptions import HTTPError  
import chardet
import hashlib
from typing import Final
from Crypto.Cipher import AES
from Crypto.Hash import CMAC
from functools import cmp_to_key


##----------------------------------------------------------------------------------------------------------

def TypedefFunction_ListComp(x, y) ->int:
    # 自定义比较函数
    if x < y:
        return -1
    elif x > y:
        return 1
    else:
        return 0

def ListSort(user_list: list, compare_func: TypedefFunction_ListComp=None, reverse: bool = False) -> list:
    """
    对列表进行排序。
    
    :param user_list: 待排序的列表
    :param compare_func: 比较函数，默认为 None
    :param reverse: 是否逆序排序，默认为 False
    :return: 排序后的列表
    """
    if compare_func is not None:
        # 将比较函数转换为关键字函数
        key = cmp_to_key(compare_func)
    else:
        key = None
    
    return sorted(user_list, key=key, reverse=reverse)


def ListFiles(path: str, show_log: bool = True, recursive: bool = False) -> list:
    """
    列出指定目录下的所有文件。
    
    :param path: 目录路径
    :param show_log: 是否显示日志，默认为 True
    :param recursive: 是否递归遍历，默认为 False
    :return: 文件路径列表
    """
    file_list = []
    
    # 如果需要递归遍历
    if recursive:
        for root, dirs, files in os.walk(path):
            for file in files:
                file_list.append(os.path.join(root, file))
                if show_log:
                    print(file)
    else:
        # 如果不需要递归遍历，则只处理当前目录
        with os.scandir(path) as entries:
            for entry in entries:
                if entry.is_file():
                    file_list.append(entry.path)
                    if show_log:
                        print(entry.name)
    return file_list


def ListDirs(path: str, show_log: bool = True) -> list:
    """
    列出指定目录下的所有目录。
    """
    dir_list = []
    for root, dirs, files in os.walk(path):
        for dir in dirs:
            dir_list.append(os.path.join(root, dir))
            if show_log:
                print(os.path.join(root, dir))
    return dir_list


##----------------------------------------------------------------------------------

def ValidateFilePath(filename: str) -> bool:
    """
    验证文件路径是否安全。简单示例仅检查是否为绝对路径且是否指向预期目录。
    根据实际需求，这里的验证可以更加严格和详细。
    """
    return os.path.isabs(filename)

def DirnameOfTheFile(filename: str) -> str:
    """
    返回给定文件名的目录部分。
    
    参数:
    filename (str): 完整的文件路径。
    
    返回:
    str: 文件所在的目录路径。
    """
    return os.path.dirname(os.path.abspath(filename))


def FileBasename(path):  
    ''' 
    @brief 获取文件的基础名称
        根据传入的文件路径，提取并返回文件的基础名称。
    @param path: str
        需要获取基础名称的文件路径。
    @return: str
        返回文件的基础名称。
    '''
    return os.path.basename(path)

##------------------------------------------------------------------

''' 
@brief 解压缩文件夹
    将指定的zip文件解压缩到指定的目录。
@param zip_filename: str
    需要解压的zip文件路径。
@param output_directory: str
    解压缩后文件存放的目录。
@return: None
该函数不返回任何值。解压出来的文件会在同名目录中
'''
def UnzipFolder(zip_filename, output_directory, create_dir_with_same_name = True, show_log=True):
    try:
        zip_basename = os.path.basename(zip_filename).split('.')[0]

        # 创建一个 ZipFile 对象
        with zipfile.ZipFile(zip_filename, 'r') as zip_ref:
            # 遍历 zip 文件中的所有文件
            for file_info in zip_ref.infolist():
                # 获取文件名
                file_name = file_info.filename
                if not create_dir_with_same_name:
                    file_name = file_name.replace(zip_basename, '/', 1)

                # 构建完整的文件路径
                filename = os.path.join(output_directory, file_name)
                
                # # 安全性检查: 防止路径遍历攻击
                # if not filename.startswith(output_directory + os.sep):
                #     raise ValueError("Attempted path traversal detected")
                
                # 如果是目录，则创建该目录
                if file_info.is_dir():
                    os.makedirs(filename, exist_ok=True)
                else:
                    # 创建文件所在的目录
                    os.makedirs(os.path.dirname(filename), exist_ok=True)
                    
                    # 写入文件内容
                    with open(filename, 'wb') as f:
                        f.write(zip_ref.read(file_info))
                if show_log:
                    print(f"解压文件: {file_name}")
    except Exception as e:
        print(f"An error occurred: {e}")

''' 
@brief 压缩文件夹
    将指定的文件夹压缩成zip文件存放在指定的路径。
@param folder_path: str
    需要压缩的文件夹路径。
@param zip_path: str
    压缩文件存放的路径。
@return: None
   该函数不返回任何值。
'''
def ZipFolder(folder_path, zip_path):
    with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
        for root, dirs, files in os.walk(folder_path):
            for file in files:
                filename = os.path.join(root, file)
                arcname = os.path.relpath(filename, folder_path)
                zipf.write(filename, arcname)

##------------------------------------------------------------------------------

def CopyFile(source_filename: str, dest_filename: str, show_log: bool = True):
    """
    将源文件复制到目标文件。
    
    参数:
        source_filename (str): 源文件的路径。
        dest_filename (str): 目标文件的路径。
        
    异常:
        FileNotFoundError: 如果源文件不存在。
        ValueError: 如果目标文件位于源文件的目录内。
        Exception: 在文件操作过程中遇到其他错误。
    """
    
    # 验证源文件
    if not os.path.isfile(source_filename):
        raise FileNotFoundError(f"源文件 {source_filename} 不存在。")
    
    # 验证并准备目标文件路径
    if os.path.commonprefix([source_filename, dest_filename]) == source_filename:
        raise ValueError("目标文件路径位于源文件路径的目录内，不安全。")
    
    # 进行文件复制并处理异常
    try:
        with open(source_filename, 'rb') as r_f:
            with open(dest_filename, 'wb') as w_f:
                # 改进的复制大文件的方法
                buffer_size = 4096  # 4KB 缓冲区
                while True:
                    chunk = r_f.read(buffer_size)
                    if not chunk:
                        break
                    w_f.write(chunk)
        if show_log:
            print(f"文件 {source_filename} 已复制到 {dest_filename}。")
    except Exception as e:
        # 可以进一步细化以处理特定异常
        raise Exception(f"在复制文件时发生错误: {str(e)}")



def CopyFolder(src: str, dst: str, show_log: bool = True) -> None:  
    """
    递归地将一个文件夹复制到另一个指定的文件夹路径，并打印复制过程。
    
    :param src: 源文件夹路径。
    :param dst: 目标文件夹路径。
    :return: None
    """
    # 确保目标目录不存在，如果存在则删除
    if os.path.exists(dst):
        shutil.rmtree(dst)
    
    # 创建目标目录
    os.makedirs(dst, exist_ok=True)
    print(f"CopyFolder '{src}' '{dst}'")
    
    for root, dirs, files in os.walk(src):
        # 复制子目录
        for dir_name in dirs:
            src_dir = os.path.join(root, dir_name)
            dst_dir = src_dir.replace(src, dst)
            os.makedirs(dst_dir, exist_ok=True)
            if show_log:
                print(f"Creating directory: {dst_dir}")
        
        # 复制文件
        for file_name in files:
            src_file = os.path.join(root, file_name)
            dst_file = src_file.replace(src, dst)
            shutil.copy2(src_file, dst_file)
            if show_log:
                print(f"Copying file: {src_file} -> {dst_file}")


def DeleteFolder(folder_path):
    if os.path.exists(folder_path):
        shutil.rmtree(folder_path)


def DeleteFile(filename):
    if os.path.exists(filename):
        os.remove(filename)


##-------------------------------------------------------------------------------------

def UrlDownload(url: str, filename = None, auth = ('username', 'password')):
    ''' 
    @brief 通过Url地址下载文件
        从指定的URL下载文件到本地指定的文件名。
    @param url: str
        下载文件的URL地址。
    @param filename: str, optional
        保存到本地的文件名。如果未指定或为空，则使用URL中的文件名。
    @param auth: tuple, optional
        可选的用户名和密码组成的元组，用于需要认证的下载。
    @return: None
        该函数不返回任何值。
    '''
    if filename == None or filename == '':
        filename = FileBasename(url)

    try:  
        response = requests.get(url)  
        response.raise_for_status()  # 如果请求返回非200状态码，将抛出HTTPError异常  
    except HTTPError as http_error:  
        print(f"HTTP error occurred: {http_error}")  
        # 在这里判断状态码，只有当状态码为401时才重新尝试下载  
        if http_error.response.status_code == 401:  
            print(f"auth = {auth}")
            response = requests.get(url, auth = auth)  
            response.raise_for_status()  # 如果请求返回非200状态码，将抛出HTTPError异常  
        else:  
            raise http_error  # 其他情况继续抛出异常
        
    try:
        total_length = response.headers.get('content-length')  
        download_size = 0  
        block_size = 1024 * 4  # 每次读取的字节数  
        progress = 0  
        fp = open(filename, 'wb')  
                
        if total_length is None:  # 无法确定文件大小，例如非分块传输编码  
            download_size = 0  
            while True:  
                data = response.raw.read(block_size)  
                if not data:  
                    break  
                else:
                    fp.write(data)
                download_size += len(data)  
                progress = download_size * 100 / (block_size * 1024)  # 假设文件大小为1KB，实际中应计算真实的文件大小并相应调整  
                sys.stdout.write('\r' + f'Downloading: {progress:.2f}% || curr: {download_size} bytes || Total Size: Unknown')  
                sys.stdout.flush()  
        else:  # 已知文件大小  
            total_length = int(total_length)  
            for data in response.iter_content(block_size):  
                if data:  
                    fp.write(data)
                    download_size += len(data)  
                    progress = download_size * 100 / total_length  
                    sys.stdout.write('\r' + f'Downloading: {progress:.2f}% || curr: {download_size} bytes || Total Size: {total_length} bytes')  
                    sys.stdout.flush() 
    finally:
        fp.close()
        response.close()  # 关闭响应的连接  
    print(f"\nThe file:{filename} was successfully downloaded from the url:{url}")  # 换行，使输出更整洁  
    return 


##-------------------------------------------------------------------------------------

def DetectFileEncoding(filename: str):
    ''' 
    @brief 检测文件编码
        读取文件的前32字节来检测文件的编码类型。
    @param filename: str
        需要检测编码的文件路径。
    @return: str
        返回检测到的文件编码类型。
    '''
    with open(filename, 'rb') as f:
        raw_data = f.read(32)  # 读取文件的前32字节来检测编码
        result = chardet.detect(raw_data)
        return result['encoding']
    


def SearchFilesExt(startpath, extension = None):
    """
    搜索指定目录下的带有指定扩展名的文件，并将这些文件的相对路径存储在一个列表中。

    参数:
    startpath (str): 起始目录路径，即从该目录开始搜索
    extension (str): 指定扩展名，例如 ".txt"

    返回:
    list: 包含搜索到的文件的相对路径的列表

    注意:
    1. 这段代码使用了`os`模块，因此需要在Python代码中导入`os`模块。
    2. 请确保传入的`startpath`是一个正确的目录路径，否则可能会导致遍历错误。
    3. 请确保传入的`extension`是一个正确的扩展名，否则可能不会搜索到任何文件。
    4. 这段代码会递归地搜索子目录中的文件，如果子目录太多，可能会导致性能问题。

    示例:
    >>> startpath = "/path/to/your/directory"
    >>> extension = ".txt"
    >>> files_with_extension = search_files_with_extension(startpath, extension)
    >>> print(files_with_extension)
    ['path/to/your/directory/file1.txt', 'path/to/your/directory/file2.txt', 'path/to/your/directory/subdirectory/file3.txt']
    """
    file_list = []

    for root, dirs, files in os.walk(startpath):
        for file in files:
            if extension == None or file.endswith(extension):
                filename = os.path.relpath(os.path.join(root, file), startpath)
                file_list.append(filename)
    print(file_list)
    return file_list

## ----------------------------------------------------------

def FileMd5(filename: str) -> str:
    """
    计算文件的MD5值。
    
    参数:
    filename (str): 文件的完整路径。

    返回:
    str: 文件的MD5散列值，以十六进制字符串形式。

    注意:
    此函数逐块读取文件，适用于大文件，避免一次性加载整个文件到内存。
    """
    hash_md5 = hashlib.md5()
    with open(filename, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()


def FileSha256(filename: str) -> str:
    """
    计算文件的SHA-256哈希值。

    @param filename: 文件的路径。
    @return: 文件的SHA-256哈希值的字符串表示。
    @raises FileNotFoundError: 如果文件不存在。
    @raises PermissionError: 如果没有足够的权限读取文件。
    """
    if not ValidateFilePath(filename):
        raise ValueError("文件路径不安全或无效")

    if not os.path.isfile(filename):
        raise FileNotFoundError(f"文件路径 '{filename}' 不存在")

    with open(filename, "rb") as file:
        sha256_hash = hashlib.sha256()
        # 分块读取文件以减少内存使用
        for chunk in iter(lambda: file.read(8192), b""):
            sha256_hash.update(chunk)
    
    return sha256_hash.hexdigest()


def FileCMac128(filename: str, key: str|bytearray|bytes) -> str|bytearray|bytes:
    '''
    计算文件的CMac值 
    @param filename     文件名
    @param key          秘钥
    @return             CMac值: 输出类型同key
    @note:xxxxx
    '''
    # 将key转换为字节类型，以便处理所有可能的输入类型
    if isinstance(key, str):
        key = key.encode('utf-8')
    elif isinstance(key, bytearray):
        key = bytes(key)
    
    # 创建CMAC对象
    cipher = AES.new(key, AES.MODE_CMAC)
    
    # 读取文件并计算CMAC
    with open(filename, 'rb') as file:
        data = file.read()
        cipher.update(data)
        
    # 获取CMAC值
    cmac_value = cipher.digest()
    
    # 返回与输入key相同类型的CMAC值
    if isinstance(key, bytes):
        return cmac_value
    elif isinstance(key, bytearray):
        return bytearray(cmac_value)
    else:
        return cmac_value.hex()



if __name__ == "__main__":
    ## 获取当前脚本所在的目录
    SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
    # 输出目录路径
    print("SCRIPT_DIR: "+ SCRIPT_DIR)

    ## 获取脚本传参
    argv = sys.argv
    argc = len(argv)
    # 打印命令行参数
    #for i in range(1, argc):
        #print("参数", i, ":", argv[i])
        
    print("============= BIGEN: ZipOpt.py =============")


    # UrlDownload("http://172.16.1.99:8081/artifactory/sei/project/wit803/r01v01/history/wit803_r01v01_1.0.1.2.zip", auth=("101706", "Auyv254579!"))
    # UrlDownload("http://172.16.1.99:8081/artifactory/sei/project/wit803/r01v01/history/wit803_r01v01_1.0.1.2.zip", "testurldownload.zip", auth=("101706", "Auyv254579!"))

    # SearchFiles(".", ".py")

    md5_value = FileMd5("D:\\AZ_Folder\\script-tools\\pyazlib\\SpcSerial.py")
    print(f"The MD5 value of the file is: {md5_value}")
    print(f"hex_bytes:{bytes.fromhex(md5_value)}")
    print(f"hex_str:{bytes.fromhex(md5_value).hex()}")

    sha256_value = FileSha256("D:\\AZ_Folder\\script-tools\\pyazlib\\SpcSerial.py")
    print(f"The Sha256 value of the file is: {sha256_value}")
    print(f"hex_bytes:{bytes.fromhex(sha256_value)}")
    print(f"hex_str:{bytes.fromhex(sha256_value).hex()}")

    print("============= END: ZipOpt.py =============")



## Generated on "2023-08-31 09:48:27" by the tool "gen_hq_file.py >> V20230509_1" 